#include "stdafx.h"

#include "BigRedButton.h"
#include "Usb.h"

BigRedButton::BigRedButton( BigRedButtonNotifications* pOwner, int refreshRate_ ) {
	this->pOwner      = pOwner;
	this->refreshRate = refreshRate_;
	hReaderThread     = nullptr;
	hWriterThread     = nullptr;
	hTimer            = nullptr;
	hShutdownEvent    = nullptr;
	hInputDevice      = INVALID_HANDLE_VALUE;
	hOutputDevice     = INVALID_HANDLE_VALUE;
	LidOpen           = false;
	ButtonPressed     = false;
}

BigRedButton::~BigRedButton( ) {
	_CleanUp( );
}

void BigRedButton::_CleanUp( void ) {
	CloseHandleIfNotEqual( hReaderThread,  nullptr );
	CloseHandleIfNotEqual( hWriterThread,  nullptr );
	CloseHandleIfNotEqual( hTimer,         nullptr );
	CloseHandleIfNotEqual( hShutdownEvent, nullptr );
	CloseHandleIfNotEqual( hInputDevice,   INVALID_HANDLE_VALUE );
	CloseHandleIfNotEqual( hOutputDevice,  INVALID_HANDLE_VALUE );
}

bool BigRedButton::_CreateWaitableTimer( void ) {
	DWORD dwLastError = ERROR_SUCCESS;

	SetLastError( 0 );
	hTimer = CreateWaitableTimer( nullptr, TRUE, nullptr );
	if ( !hTimer ) {
		dwLastError = GetLastError( );
		debug( L"BigRedButton::_CreateWaitableTimer: CreateWaitableTimer failed, error %ld\n", dwLastError );
		return false;
	}

	int timerPeriod = 1000 / refreshRate;
	LARGE_INTEGER li;
	li.QuadPart = -timerPeriod;

	SetLastError( 0 );
	if ( !SetWaitableTimer( hTimer, &li, timerPeriod, nullptr, nullptr, FALSE ) ) {
		dwLastError = GetLastError( );
		debug( L"BigRedButton::_CreateWaitableTimer: SetWaitableTimer failed, error %ld\n", dwLastError );
		CloseHandle( hTimer );
		hTimer = nullptr;
		return false;
	}

	return true;
}

void BigRedButton::_RaiseLidStateChange( bool lidOpen ) {
	if ( pOwner ) {
		pOwner->BLidStateChange( lidOpen );
	}
}

void BigRedButton::_RaiseButtonStateChange( bool buttonPressed ) {
	if ( pOwner ) {
		pOwner->BButtonStateChange( buttonPressed );
	}
}

bool BigRedButton::_StartReaderThread( void ) {
	hReaderThread = reinterpret_cast<HANDLE>( _beginthreadex( nullptr, 0, _ReaderThunk, this, 0, nullptr ) );
	return nullptr != hReaderThread;
}

bool BigRedButton::_StartWriterThread( void ) {
	hWriterThread = reinterpret_cast<HANDLE>( _beginthreadex( nullptr, 0, _WriterThunk, this, 0, nullptr ) );
	return nullptr != hWriterThread;
}

unsigned __stdcall BigRedButton::_ReaderThunk( void* pv ) {
	HRESULT hr = CoInitializeEx( nullptr, COINIT_MULTITHREADED );
	if ( FAILED( hr ) ) {
		debug( L"BigRedButton::_ReaderThunk: CoInitializeEx failed, 0x%08x\n", hr );
		return 0;
	}

	reinterpret_cast<BigRedButton*>( pv )->_ReaderThread( );

	CoUninitialize( );
	return 0;
}

unsigned __stdcall BigRedButton::_WriterThunk( void* pv ) {
	HRESULT hr = CoInitializeEx( nullptr, COINIT_MULTITHREADED );
	if ( FAILED( hr ) ) {
		debug( L"BigRedButton::_WriterThunk: CoInitializeEx failed, 0x%08x\n", hr );
		return 0;
	}

	reinterpret_cast<BigRedButton*>( pv )->_WriterThread( );

	CoUninitialize( );
	return 0;
}

void BigRedButton::_ReaderThread( void ) {
	DWORD dwLastError;

	SetLastError( 0 );
	HANDLE hInputEvent = CreateEvent( nullptr, TRUE, FALSE, nullptr );
	if ( !hInputEvent ) {
		dwLastError = GetLastError( );
		debug( L"BigRedButton::_ReaderThread: fatal: can't create input event, error %ld\n", dwLastError );
		return;
	}

	OVERLAPPED overlapped;
	ZeroMemory( overlapped );
	overlapped.hEvent = hInputEvent;

	UINT8* InputReport = new UINT8[ Capabilities.InputReportByteLength + 1 ];
	ZeroMemory( InputReport, Capabilities.InputReportByteLength + 1 );

	HANDLE hWaiters[2] = { hShutdownEvent, hInputEvent };
	bool readPending = false;
	DWORD dwBytesRead;
	int nReports = 0;
	DWORD rc;

	while ( true ) {
		if ( !readPending ) {
			SetLastError( 0 );
			if ( !ReadFile( hInputDevice, InputReport, Capabilities.InputReportByteLength, &dwBytesRead, &overlapped ) ) {
				dwLastError = GetLastError( );
				if ( ERROR_IO_PENDING == dwLastError ) {
					readPending = true;
				} else {
					debug( L"BigRedButton::_ReaderThread: fatal: ReadFile failed, error %ld\n", dwLastError );
					goto out;
				}
			} else {
				if ( dwBytesRead < Capabilities.InputReportByteLength ) {
					dwLastError = GetLastError( );
					debug( L"BigRedButton::_ReaderThread: Short read? %ld read vs %ld requested, error %ld\n", dwBytesRead, Capabilities.InputReportByteLength, dwLastError );
					continue;
				}
			}
		}

		SetLastError( 0 );
		rc = WaitForMultipleObjects( 2, hWaiters, FALSE, INFINITE );

		switch ( rc ) {
			case 0:
				// hShutdownEvent
				debug( L"BigRedButton::_ReaderThread: shutdown event received, terminating\n" );
				goto out;

			case 1:
				// hInputEvent
				readPending = false;
				ResetEvent( hInputEvent );
				break;

			default:
				dwLastError = GetLastError( );
				debug( L"BigRedButton::_ReaderThread: fatal: WaitForMultipleObjects returned 0x%lX, error is %ld\n", rc, dwLastError );
				goto out;
		}

		bool new_LidOpen       = ( InputReport[1] & 2 ) != 0;
		bool new_ButtonPressed = ( InputReport[1] & 1 ) == 0;

		bool printReport = false;

		if ( new_LidOpen != LidOpen ) {
			printReport = true;
			LidOpen = new_LidOpen;
			_RaiseLidStateChange( LidOpen );
		}
		if ( new_ButtonPressed != ButtonPressed ) {
			printReport = true;
			ButtonPressed = new_ButtonPressed;
			_RaiseButtonStateChange( ButtonPressed );
		}
		if ( printReport ) {
			debug(
				L"BigRedButton::_ReaderThread: Input report #%d: 0x%02x: lid %s button %s\n",
				nReports++,
				InputReport[1],
				new_LidOpen       ? L"OPEN"    : L"closed",
				new_ButtonPressed ? L"PRESSED" : L"up"
			);
		}
	}

out:
	SetLastError( 0 );
	if ( !CancelIo( hInputDevice ) ) {
		dwLastError = GetLastError( );
		debug( L"BigRedButton::_ReaderThread: CancelIo failed, error %ld\n", dwLastError );
	}

	CloseHandleIfNotEqual( hInputEvent, nullptr );
	delete[] InputReport;
}

void BigRedButton::_WriterThread( void ) {
	HANDLE hWaiters[2] = { hShutdownEvent, hTimer };
	DWORD dwBytesWritten = 0;
	DWORD dwLastError;
	DWORD rc;

	UINT8* OutputReport = new UINT8[ Capabilities.OutputReportByteLength + 1 ];
	ZeroMemory( OutputReport, Capabilities.OutputReportByteLength + 1 );
	OutputReport[8] = 2;

	while ( true ) {
		SetLastError( 0 );
		rc = WaitForMultipleObjects( 2, hWaiters, FALSE, INFINITE );

		switch ( rc ) {
			case 0: // hShutdownEvent
				debug( L"BigRedButton::_WriterThread: shutdown event received, terminating\n" );
				goto out;

			case 1: // hTimer, do write
				ResetEvent( hTimer );
				break;

			default:
				dwLastError = GetLastError( );
				debug( L"BigRedButton::_WriterThread: fatal: WaitForMultipleObjects returned 0x%lX, error %ld\n", rc, dwLastError );
				goto out;
		}

		SetLastError( 0 );
		if ( !WriteFile( hOutputDevice, OutputReport, Capabilities.OutputReportByteLength, &dwBytesWritten, nullptr ) ) {
			dwLastError = GetLastError( );
			debug( L"BigRedButton::_WriterThread: fatal: WriteFile failed, error %ld\n", dwLastError );
			goto out;
		}
		if ( dwBytesWritten < Capabilities.OutputReportByteLength ) {
			dwLastError = GetLastError( );
			debug( L"BigRedButton::_WriterThread: Short write? %ld written vs %ld offered, error %ld\n", dwBytesWritten, Capabilities.OutputReportByteLength, dwLastError );
		}
	}

out:
	delete[] OutputReport;
}

bool BigRedButton::Open( void ) {
	DWORD dwLastError = ERROR_SUCCESS;

	if ( IsOpen( ) ) {
		debug( L"BigRedButton::Open: Duplicate Open() attempted\n" );
		return false;
	}

	if ( !OpenHidDevice( 0x1D34, 0x000D, 2, hInputDevice, hOutputDevice ) ) {
		debug( L"BigRedButton::Open: OpenHidDevice failed\n" );
		BreakIfDebuggerPresent( );
		goto fail;
	}

	if ( !GetDeviceCapabilities( hInputDevice, Capabilities ) ) {
		debug( L"BigRedButton::Open: GetDeviceCapabilities failed\n" );
		BreakIfDebuggerPresent( );
		goto fail;
	}

	SetLastError( 0 );
	hShutdownEvent = CreateEvent( nullptr, TRUE, FALSE, nullptr );
	if ( !hShutdownEvent ) {
		dwLastError = GetLastError( );
		debug( L"BigRedButton::Open: fatal: can't create shutdown event, error %ld\n", dwLastError );
		goto fail;
	}

	if ( !_CreateWaitableTimer( ) ) {
		debug( L"BigRedButton::Open: fatal: can't create waitable timer\n" );
		goto fail;
	}

	if ( !_StartReaderThread( ) ) {
		debug( L"BigRedButton::Open: fatal: can't start reader thread\n" );
		Close( );
		return false;
	}

	if ( !_StartWriterThread( ) ) {
		debug( L"BigRedButton::Open: fatal: can't start writer thread\n" );
		Close( );
		return false;
	}

	return true;

fail:
	_CleanUp( );
	return false;
}

bool BigRedButton::Close( void ) {
	if ( !IsOpen( ) ) {
		debug( L"BigRedButton::Close: Duplicate Close() attempted\n" );
		return false;
	}

	pOwner = nullptr;

	HANDLE hWaiters[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE };
	int cWaiters = 0;

	if ( INVALID_HANDLE_VALUE != hReaderThread ) {
		hWaiters[cWaiters++] = hReaderThread;
	}
	if ( INVALID_HANDLE_VALUE != hWriterThread ) {
		hWaiters[cWaiters++] = hWriterThread;
	}

	if ( cWaiters ) {
		SetEvent( hShutdownEvent );
		WaitForMultipleObjects( cWaiters, hWaiters, TRUE, INFINITE );
	}

	CancelWaitableTimer( hTimer );

	_CleanUp( );

	return true;
}

bool BigRedButton::SetRefreshRate( int const newRefreshRate ) {
	int savedRefreshRate = refreshRate;
	refreshRate = newRefreshRate;

	int timerPeriod = 1000 / refreshRate;
	LARGE_INTEGER li;
	li.QuadPart = -timerPeriod;

	SetLastError( 0 );
	if ( !SetWaitableTimer( hTimer, &li, timerPeriod, nullptr, nullptr, FALSE ) ) {
		DWORD dwLastError = GetLastError( );
		debug( L"BigRedButton::SetRefreshRate: SetWaitableTimer failed, error %ld\n", dwLastError );
		refreshRate = savedRefreshRate;
		return false;
	}

	return true;
}
